Web and WCF services are simple and
effective ways of implementing a server API. When the ASP.NET AJAX
runtime engine has generated the proxy class, you’re pretty much done
and can start calling methods as if they were local to the client. Web
and WCF services, though, are not free of issues. They require an extra
layer of code and additional files or assembly references to be added to
the project. Is this a big source of concern for you? If so, consider
that you have an alternative—page methods.
Introducing Page Methods
Page methods are simply public, static methods exposed by the code-behind class of a given page and decorated with the WebMethod
attribute. The runtime engine for page methods and AJAX-enabled Web
services is nearly the same. Using page methods saves you from the
burden of creating and publishing a service; at the same time, though,
it binds you to having page-scoped methods that can’t be called from
within a page different from the one where they are defined. We’ll
return later to the pros and cons of page methods. For now, let’s just
learn more about them.
Defining a Page Method
Public and static methods defined on a page’s code-behind class and flagged with the WebMethod attribute transform an ASP.NET AJAX page into a Web service. Here’s a sample page method:
public class TimeServicePage : System.Web.UI.Page
{
[WebMethod]
public static DateTime GetTime()
{
return DateTime.Now;
}
}
You can use any
data type in the definition of page methods, including .NET Framework
types as well as user-defined types. All types will be transparently
JSON-serialized during each call.
Note
The
page class where you define methods might be the direct code-behind
class or, better yet, a parent class. In this way, in the parent class
you can implement the contract of the public server API and keep it
somewhat separated from the rest of event handlers and methods that are
specific to the page life cycle and behavior. Because page methods are
required to be static (shared
in Microsoft Visual Basic .NET), you can’t use the syntax of interfaces
to define the contract. You have to resort to abstract base classes. |
Alternatively, you can define Web methods as inline code in the .aspx source file as follows (and if you use Visual Basic, just change the type attribute to text/VB):
<script type="text/C#" runat="server">
[WebMethod]
public static DateTime GetTime()
{
return DateTime.Now;
}
</script>
Note that page methods
are specific to a given ASP.NET page. Only the host page can call its
methods. Cross-page method calls are not supported. If they are critical
for your scenario, I suggest that you move to using Web or WCF
services.
Enabling Page Methods
When the code-behind class of an ASP.NET AJAX page contains WebMethod-decorated
static methods, the runtime engine emits a JavaScript proxy class
nearly identical to the class generated for a Web service. You use a
global instance of this class to call server methods. The name of the
class is hard-coded to PageMethods. We’ll return to the characteristics of the proxy class in a moment.
Note, however, that page methods are not enabled by default. In other words, the PageMethods proxy class that you use to place remote calls is not generated unless you set the EnablePageMethods property to true in the page’s script manager:
<asp:ScriptManager runat="server" ID="ScriptManager1" EnablePageMethods="true" />
For the successful execution of a page method, the ASP.NET AJAX application must have the ScriptModule HTTP module enabled in the web.config file:
<httpModules>
<add name="ScriptModule"
type="System.Web.Handlers.ScriptModule, System.Web.Extensions" />
</httpModules>
Among other
things, the module intercepts the application event that follows the
loading of the session state, executes the method, and then serves the
response to the caller. Acquiring session state is the step that
precedes the start of the page life cycle. For page method calls,
therefore, there’s no page life cycle and child controls are not
initialized and processed.
Why No Page Life Cycle?
In the early days of
ASP.NET AJAX (when it was code-named Atlas), page methods were instance
methods and required view state and form fields to be sent with every
call. The sent view state was the last known good view state for the
page—that is, the view state downloaded to the client. It was common for
developers to expect that during the page method execution, say, a TextBox
was set to the same text just typed before triggering the remote call.
Because the sent view state was the last known good view state, that
expectation was just impossible to meet. At the same time, a large share
of developers was also complaining that the view state was being sent
at all during page method calls. View state is rarely small, which
serves to increase the bandwidth and processing requirements for
handling page methods.
In the end,
ASP.NET AJAX extensions require static methods and execute them just
before starting the page life cycle. The page request is processed as
usual until the session state is retrieved. After that, instead of the
page method call going through the page life cycle, the HTTP module
kicks in, executes the method via reflection, and returns.
Coded in this way,
the execution of a remote page method is quite effective and nearly
identical to having a local Web service up and running. The fact that
static methods are used and no page life cycle is ever started means one
thing to you—you can’t programmatically access page controls and their
properties.
Consuming Page Methods
The collection of page methods is exposed to the JavaScript code as a class with a fixed name—PageMethods.
The schema of this class is similar to the schema of proxy classes for
AJAX-enabled Web services. The class lists static methods and doesn’t
require any instantiation on your own. Let’s take a look at the PageMethods class.
The Proxy Class
Unlike the proxy class for Web services, the PageMethods
proxy class is always generated as inline script in the body of the
page it refers to. That’s a fairly obvious choice given the fixed naming
convention in use; otherwise, the name of the class should be different
for each page. Here’s the source code of the PageMethods class for a page with just one Web method, named GetTime:
<script type="text/javascript">
var PageMethods = function()
{
PageMethods.initializeBase(this);
this._timeout = 0;
this._userContext = null;
this._succeeded = null;
this._failed = null;
}
PageMethods.prototype =
{
GetTime:function(succeededCallback, failedCallback, userContext)
{
return this._invoke(PageMethods.get_path(),
'GetTime', false, {}, succeededCallback,
failedCallback, userContext);
}
}
PageMethods.registerClass('PageMethods', Sys.Net.WebServiceProxy);
PageMethods._staticInstance = new PageMethods();
PageMethods.set_path = function(value) {
var e = Function._validateParams(arguments,
[{name: 'path', type: String}]);
if (e) throw e;
PageMethods._staticInstance._path = value;
}
PageMethods.get_path = function() {
return PageMethods._staticInstance._path;
}
PageMethods.set_timeout = function(value) {
var e = Function._validateParams(arguments,
[{name: 'timeout', type: Number}]);
if (e) throw e;
if (value < 0)
throw Error.argumentOutOfRange('value', value,
Sys.Res.invalidTimeout);
PageMethods._staticInstance._timeout = value;
}
PageMethods.get_timeout = function() {
return PageMethods._staticInstance._timeout;
}
PageMethods.set_defaultUserContext = function(value) {
PageMethods._staticInstance._userContext = value;
}
PageMethods.get_defaultUserContext = function() {
return PageMethods._staticInstance._userContext;
}
PageMethods.set_defaultSucceededCallback = function(value) {
var e = Function._validateParams(arguments,
[{name: 'defaultSucceededCallback', type: Function}]);
if (e) throw e;
PageMethods._staticInstance._succeeded = value;
}
PageMethods.get_defaultSucceededCallback = function() {
return PageMethods._staticInstance._succeeded;
}
PageMethods.set_defaultFailedCallback = function(value) {
var e = Function._validateParams(arguments,
[{name: 'defaultFailedCallback', type: Function}]);
if (e) throw e;
PageMethods._staticInstance._failed = value;
}
PageMethods.get_defaultFailedCallback = function() {
return PageMethods._staticInstance._failed;
}
PageMethods.set_path("/Core35/Ch20/CallPageMethod.aspx");
PageMethods.GetTime = function(onSuccess,onFailed,userContext) {
PageMethods._staticInstance.GetTime(onSuccess,onFailed,userContext);
}
</script>
As
you can see, the structure of the class is nearly identical to the
proxy class of an AJAX Web service. You can define default callbacks for
success and failure, user context data, path, and timeout. A singleton
instance of the PageMethods class is created, and all callable methods are invoked through this static instance. No instantiation whatsoever is required.
Executing Page Methods
The PageMethods
proxy class has as many methods as there are Web methods in the
code-behind class of the page. In the proxy class, each mapping method
takes the same additional parameters you would find with a Web service
method: completed callback, failed callback, and user context data. The
completed callback is necessary to update the page with the results of
the call. The other parameters are optional. The following code snippet
shows a locally-defined getTime function bound to a client event handler. The function calls a page method and leaves the methodCompleted callback the burden of updating the user interface as appropriate.
function getTime()
{
PageMethods.GetTime(methodCompleted);
}
function methodCompleted(results, context, methodName)
{
// Format the date-time object to a more readable string
var displayString = results.format("ddd, dd MMMM yyyy");
$get("Label1").innerHTML = displayString;
}
The signature of a page method callback is exactly the same as the signature of an AJAX Web service proxy.
Timeout, error
handling, and user feedback are all aspects of page methods that require
the same programming techniques discussed earlier for Web service
calls.
Note
From page methods, you can access session state, the ASP.NET Cache, and User objects, as well as any other intrinsic objects. You can do that using the Current property on HttpContext.
The HTTP context is not specific to the page life cycle and is,
instead, a piece of information that accompanies the request from the
start. |
Page Methods vs. AJAX-Enabled Services
From a
programming standpoint, no difference exists between service methods and
page methods. Performance is nearly identical. A minor difference is
the fact that page methods are always emitted as inline JavaScript,
whereas this aspect is configurable for services.
Web
services are publicly exposed over the Web and, as such, they’re
publicly callable by SOAP-based clients (unless the protocol is
disabled). A method exposed through a Web or WCF service is visible from
multiple pages; a page method, conversely, is scoped to the page that
defines it. On the other hand, a set of page methods saves you from the
additional work of developing a service.
Whatever choice you make,
it is extremely important that you don’t call any critical business
logic from page and service methods. Both calls can be easily replayed
by attackers and have no additional barrier against one-click and replay
attacks. Normally, the view state, when spiced up with user key values,
limits the range of replay attacks. As mentioned, though, there’s no
view state involved with page and Web service method calls, so even this
small amount of protection isn’t available for these specific cases.
However, if you limit your code to calling UI-level business logic from
the client, you should be fine.
Note
I
repeatedly mentioned that AJAX-enabled services, including Web
services, are to be considered local to the application. They are in
fact application services implemented as ASP.NET Web services because of
the lack of alternatives. With ASP.NET 3.5, though, you have the
possibility of using WCF services. What if you want to incorporate data
coming from a classic WS-* Web service? You can’t invoke the Web service
directly from the client, but nothing prevents you from making a
server-to-server call using the networking API of the .NET Framework. |